﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="GaugeChart.cs" company="NoCompany">
//   
// </copyright>
// <summary>
//   The gauge chart.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Globalization;
using System.Linq;
using Graphix.Elements;

namespace Graphix.Charts
{
    using System.IO;
    using System.Reflection;

    using CommonHelpers;

    using Graphix.Charts.Base;

    /// <summary>
    ///   The gauge chart.
    /// </summary>
    public class GaugeChart : Core
    {
        public SimpleAxis Axis { get; set; }

        #region Constants

        /// <summary>
        ///   Standard maximum rotation.
        /// </summary>
        private const float Max = 268;

        /// <summary>
        ///   Standard minimum rotation.
        /// </summary>
        private const float Min = 136;

        #endregion

        #region Fields

        /// <summary>
        ///   Beginning of range for "green" color.
        /// </summary>
        private float greenFrom;

        /// <summary>
        ///   Ending of range for "green" color.
        /// </summary>
        private float greenTo;

        /// <summary>
        ///   Beginning of range for "red" color.
        /// </summary>
        private float redFrom;

        /// <summary>
        ///   Ending of range for "red" color.
        /// </summary>
        private float redTo;

        /// <summary>
        ///   Beginning of range for "yellow" color.
        /// </summary>
        private float yellowFrom;

        /// <summary>
        ///   Ending of range for "yellow" color.
        /// </summary>
        private float yellowTo;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        ///   Initializes a new instance of the <see cref="GaugeChart" /> class.
        /// </summary>
        public GaugeChart()
        {
            this.Easing = Easing.Linear;
            this.Axis = new Axis();

            this.GreenColor = Color.Green;
            this.YellowColor = Color.Yellow;
            this.RedColor = Color.FromArgb((int)(255 * 0.6), Color.Red);

            this.EvenTickPen = Pens.SaddleBrown;
            this.OddTickPen = new Pen(Color.SaddleBrown, 2);

            this.FillArrowColor = new SolidBrush(Color.FromArgb(179, 220, 27, 18));
            this.BorderArrowColor = new Pen(Color.FromArgb(179, 198, 51, 10));
            this.FillArrowDotColor = new SolidBrush(Color.FromArgb(255, 70, 132, 238));
            this.BorderArrowDotColor = new Pen(Color.FromArgb(255, 102, 102, 102));

            this.TicksColor = new SolidBrush(Color.Black);

            this.MainInsideDiscColor = Brushes.White;

            this.MainOutsideDiscColor = new SolidBrush(Color.FromArgb(255, 204, 204, 204));
            this.MainOutsideDiscBorderColor = new Pen(Color.FromArgb(255, 51, 51, 51));

            this.Width = 100;
            this.Height = 100;
            this.MinorTicks = 1;
            this.Values = new List<float>() { 0 };
        }

        #endregion

        #region Public Properties

        /// <summary>
        ///   Gets or sets the border arrow color.
        /// </summary>
        public Pen BorderArrowColor { get; set; }

        /// <summary>
        ///   Gets or sets the border arrow dot color.
        /// </summary>
        public Pen BorderArrowDotColor { get; set; }

        /// <summary>
        ///   Gets or sets the even tick pen.
        /// </summary>
        public Pen EvenTickPen { get; set; }

        /// <summary>
        ///   Gets or sets the fill arrow color.
        /// </summary>
        public Brush FillArrowColor { get; set; }

        /// <summary>
        ///   Gets or sets the fill arrow dot color.
        /// </summary>
        public Brush FillArrowDotColor { get; set; }

        /// <summary>
        ///   Gets or sets the "green" color.
        /// </summary>
        public Color GreenColor { get; set; }

        /// <summary>
        ///   Gets or sets the beginning position of "green" color.
        /// </summary>
        public float GreenFrom
        {
            get
            {
                return this.greenFrom;
            }

            set
            {
                this.greenFrom = FitValue(value);
            }
        }

        /// <summary>
        ///   Gets or sets the ending position of "green" color.
        /// </summary>
        public float GreenTo
        {
            get
            {
                return this.greenTo;
            }

            set
            {
                this.greenTo = FitValue(value);
            }
        }

        /// <summary>
        ///   Gets or sets the color of main inside disc.
        /// </summary>
        public Brush MainInsideDiscColor { get; set; }

        /// <summary>
        ///   Gets or sets the color of main outside disc border.
        /// </summary>
        public Pen MainOutsideDiscBorderColor { get; set; }

        /// <summary>
        ///   Gets or sets the color of main outside disc.
        /// </summary>
        public Brush MainOutsideDiscColor { get; set; }

        /// <summary>
        ///   Gets or sets the major ticks.
        /// </summary>
        public string[] MajorTicks { get; set; }

        /// <summary>
        ///   Gets or sets the minor ticks.
        /// </summary>
        public int MinorTicks { get; set; }

        /// <summary>
        ///   Gets or sets the odd tick pen.
        /// </summary>
        public Pen OddTickPen { get; set; }

        /// <summary>
        ///   Gets or sets the "red" color.
        /// </summary>
        public Color RedColor { get; set; }

        /// <summary>
        ///   Gets or sets the beginning position of "red" color.
        /// </summary>
        public float RedFrom
        {
            get
            {
                return this.redFrom;
            }

            set
            {
                this.redFrom = FitValue(value);
            }
        }

        /// <summary>
        ///   Gets or sets the ending position of "red" color.
        /// </summary>
        public float RedTo
        {
            get
            {
                return this.redTo;
            }

            set
            {
                this.redTo = FitValue(value);
            }
        }

        public float Maximum { get; set; }

        public float Minimum { get; set; }

        private float FitValue(float value)
        {
            if (value < Minimum)
            {
                value = Minimum;
            }
            else if (value > Maximum)
            {
                value = Maximum;
            }

            return value;
        }

        /// <summary>
        ///   Gets or sets the color of font for ticks.
        /// </summary>
        public Brush TicksColor { get; set; }

        /// <summary>
        ///   Gets or sets the displayed value.
        /// </summary>
        public List<float> Values { get; set; }

        /// <summary>
        ///   Gets or sets the "yellow" color.
        /// </summary>
        public Color YellowColor { get; set; }

        /// <summary>
        ///   Gets or sets the beginning position of "yellow" color.
        /// </summary>
        public float YellowFrom
        {
            get
            {
                return this.yellowFrom;
            }

            set
            {
                this.yellowFrom = FitValue(value);
            }
        }

        /// <summary>
        ///   Gets or sets the ending position of "yellow" color.
        /// </summary>
        public float YellowTo
        {
            get
            {
                return this.yellowTo;
            }

            set
            {
                this.yellowTo = FitValue(value);
            }
        }

        #endregion

        #region Methods

        /// <summary>
        ///   Draws final layout of chart.
        /// </summary>
        protected override void DrawChart()
        {
            float thickness = (float)Math.Round(12f + (13.5f * ((this.Height - 100f) / 100f)));
            float move = (float)Math.Round(4.5f * (this.Height / 100f)) * 2;

            this.AddMainDisc(move);
            this.AddColoredDiscParts(move, thickness);
            this.AddTicks(move, thickness);
            this.AddArrow();
            this.AddValue(move);
        }

        /// <summary>
        ///   Adds arrow to disc.
        /// </summary>
        private void AddArrow()
        {
            float min = Math.Abs(this.Minimum);
            float range = Math.Abs(this.Minimum) + Math.Abs(this.Maximum);

            float halfWidth = this.Width / 2f;
            float halfHeight = this.Height / 2f;

            // MAGIC NUMBERS
            // Drawning arrow
            PointF[] points = new[]
                {
                    new PointF((0.5f * this.Height) - 2f, 
                        (0.115394f * this.Height) + 4.26667f), 
                    new PointF((0.528322f * this.Height) - 2.43867f, 
                        (0.5f * this.Height) - 2f), 
                    new PointF((0.514161f * this.Height) - 2.21733f, 
                        (0.622303f * this.Height) - 3.93333f), 
                    new PointF((0.5f * this.Height) - 2f, 
                        (0.622303f * this.Height) - 3.93333f), 
                    new PointF((0.485839f * this.Height) - 1.77867f, 
                        (0.622303f * this.Height) - 3.93333f), 
                    new PointF((0.471678f * this.Height) - 1.56133f, 
                        (0.5f * this.Height) - 2f), 
                    new PointF((0.5f * this.Height) - 2f, 
                        (0.115394f * this.Height) + 4.26667f)
                };

            for (int i = 0; i < points.Length; i++)
            {
                points[i].X += 2;
                points[i].Y += 2;
            }

            Matrix rotationTransform = new Matrix(1, 0, 0, 1, 0, 0);
            PointF theRotationPoint = new PointF(halfWidth, halfHeight);

            float tempValue = this.Values[0];
            if (tempValue > this.Maximum + 10)
            {
                tempValue = this.Maximum + 10;
            }
            else if (tempValue < this.Minimum - 10)
            {
                tempValue = this.Minimum - 10;
            }

            rotationTransform.RotateAt(Max * (tempValue / range), theRotationPoint);

            Polygon arrow = new Polygon(points, this.FillArrowColor, this.BorderArrowColor);
            arrow.Transformation = rotationTransform;
            this.AddElement(arrow);

            // changing location of move
            float move = (float)Math.Round(6f * (this.Height / 100f)) * 2;
            float halfMove = move / 2f;

            // draw middle circle
            Dot dot = new Dot(halfWidth - halfMove,
                halfHeight - halfMove,
                move,
                this.FillArrowDotColor);
            dot.BorderColor = this.BorderArrowDotColor;
            this.AddElement(dot);
        }

        /// <summary>
        /// Adds colored parts of disc, to be exact "green", "yellow" and "red" ranges on disc.
        /// </summary>
        /// <param name="move">
        /// Move parameter generated in <see cref="DrawChart"/> 
        /// </param>
        /// <param name="thickness">
        /// Thickness of colored parts. 
        /// </param>
        private void AddColoredDiscParts(float move, float thickness)
        {
            float min = Math.Abs(this.Minimum);
            float range = Math.Abs(this.Minimum) + Math.Abs(this.Maximum);
            float middleOfDisc = 0 + (move / 2) + (thickness / 2);
            PointF middle = new PointF(middleOfDisc, middleOfDisc);
            Arc arc;

            // todo float fix?
            float from = this.GreenFrom + min;
            float to = this.GreenTo + min;
            arc = new Arc(middle,
                this.Width - move - thickness,
                this.Height - move - thickness,
                Min + ((from / range) * Max),
                (to - from) / range * Max,
                thickness);
            arc.Color = new SolidBrush(this.GreenColor);

            this.AddElement(arc);

            from = this.YellowFrom + min;
            to = this.YellowTo + min;
            arc = new Arc(middle,
                this.Width - move - thickness,
                this.Height - move - thickness,
                Min + ((from / range) * Max),
                (to - from) / range * Max,
                thickness);
            arc.Color = new SolidBrush(this.YellowColor);

            this.AddElement(arc);

            from = this.RedFrom + min;
            to = this.RedTo + min;
            arc = new Arc(middle,
                this.Width - move - thickness,
                this.Height - move - thickness,
                Min + ((from / range) * Max),
                (to - from) / range * Max,
                thickness);
            arc.Color = new SolidBrush(this.RedColor);

            this.AddElement(arc);

        }

        /// <summary>
        /// Adds main part of chart.
        /// </summary>
        /// <param name="move">
        /// Move parameter generated in <see cref="DrawChart"/> 
        /// </param>
        private void AddMainDisc(float move)
        {
            Dot dot = new Dot(0, 0, this.Width - 2, this.MainOutsideDiscColor);
            dot.BorderColor = this.MainOutsideDiscBorderColor;
            this.AddElement(dot);

            float halfmove = move / 2;
            dot = new Dot(0 + halfmove, 0 + halfmove, this.Width - move, this.MainInsideDiscColor);
            this.AddElement(dot);
        }

        /// <summary>
        /// Adds ticks on a disc.
        /// </summary>
        /// <param name="move">
        /// Move parameter generated in <see cref="DrawChart"/> 
        /// </param>
        /// <param name="thickness">
        /// Thickness of colored parts on a disc. 
        /// </param>
        private void AddTicks(float move, float thickness)
        {
            StringFormat format = new StringFormat(StringFormat.GenericTypographic)
                {
                    LineAlignment = StringAlignment.Center,
                    Alignment = StringAlignment.Center
                };

            if (this.MajorTicks == null || this.MajorTicks.Length == 0)
            {
                float min = this.Minimum;
                var increase = (Math.Abs(min) + Math.Abs(this.Maximum)) / 10f;
                this.MajorTicks = Enumerable.Range(0, 11).Select(x => ((x * increase) + min).ToString()).ToArray();
            }

            // 0 degrees = 90
            // starting from a point which guarantees that some point will end up at 270 degrees
            int numElements = this.MajorTicks.Length;

            float step = Max / (numElements - 1);
            float angle = Min;
            float halfHeight = this.Height / 2f;
            float halfWidth = this.Width / 2f;
            float halfMove = move / 2;
            float thinner = thickness * 0.8f;

            for (int i = 0; i < numElements; i++)
            {
                double tmp = StaticHelpers.DegreeToRadian(angle);
                float tmpCos = (float)Math.Cos(tmp);
                float tmpSin = (float)Math.Sin(tmp);

                float x1 = halfWidth - (halfMove * tmpCos) + (halfWidth * tmpCos);
                float y1 = halfHeight - (halfMove * tmpSin) + (halfHeight * tmpSin);
                float x2 = halfWidth - ((halfMove + thinner) * tmpCos) + (halfWidth * tmpCos);
                float y2 = halfHeight - ((halfMove + thinner) * tmpSin) + (halfHeight * tmpSin);

                this.AddElement(new Line(x1, y1, x2, y2, this.EvenTickPen));

                float x3 = halfWidth - ((halfMove + (thinner * 1.6f)) * tmpCos)
                           + (halfWidth * tmpCos);
                float y3 = halfHeight - ((halfMove + (thinner * 1.6f)) * tmpSin)
                           + (halfHeight * tmpSin);

                ChartString cs = new ChartString(x3, y3, 0, 0,
                    string.Format(this.Axis.Format, this.MajorTicks[i]), this.Axis.TextStyle.Font,
                    this.Axis.TextStyle.ColorBrush);
                cs.Format = format;
                this.AddElement(cs);

                angle += step;
            }

            // minor ticks
            if (this.MinorTicks >= 1)
            {
                int tempTicks = this.MinorTicks + 1;
                step /= tempTicks;
                angle = Min;
                int loops = (numElements * tempTicks) - tempTicks;
                for (int i = 0; i < loops; i++)
                {
                    double tmp = StaticHelpers.DegreeToRadian(angle);
                    float tmpCos = (float)Math.Cos(tmp);
                    float tmpSin = (float)Math.Sin(tmp);

                    float x1 = halfWidth - (halfMove * tmpCos) + (halfWidth * tmpCos);
                    float y1 = halfHeight - (halfMove * tmpSin) + (halfHeight * tmpSin);
                    float x2 = halfWidth - ((halfMove + (thinner / 2f)) * tmpCos)
                               + (halfWidth * tmpCos);
                    float y2 = halfHeight - ((halfMove + (thinner / 2f)) * tmpSin)
                               + (halfHeight * tmpSin);

                    if (i % tempTicks > 0)
                    {
                        this.AddElement(new Line(x1, y1, x2, y2, this.OddTickPen));
                    }

                    angle += step;
                }
            }
        }


        /// <summary>
        /// Adds displayed value on chart.
        /// </summary>
        /// <param name="move">
        /// Move parameter generated in <see cref="DrawChart"/> 
        /// </param>
        private void AddValue(float move)
        {
            StringFormat format = new StringFormat(StringFormat.GenericTypographic)
                {
                    LineAlignment = StringAlignment.Center,
                    Alignment = StringAlignment.Center
                };

            Font descriptionFont = new Font("Arial", this.Height / 10f, FontStyle.Regular);
            string value = this.Values[0].ToString(CultureInfo.InvariantCulture);

            RectangleF size = DefaultGraphics.Instance.MeasureDisplayString(value, descriptionFont);
            this.AddElement(
                new ChartString(this.Width / 2f,
                    this.Height - move - (size.Height / 2),
                    size.Width,
                    size.Height,
                    value,
                    descriptionFont)
                    {
                        Format = format
                    });
        }

        /// <summary>
        ///   Fixes ranges for colored ranges on a disc.
        /// </summary>
        private void FixRanges()
        {
            // Move beginning of yellow to acceptable starting point
            if (this.GreenTo
                > this.YellowFrom)
            {
                this.YellowFrom = this.GreenTo;
            }

            // Move beginning of red to acceptable starting point
            if (this.GreenTo
                > this.RedFrom)
            {
                this.RedFrom = this.GreenTo;
            }

            if (this.YellowTo
                > this.RedFrom)
            {
                this.RedFrom = this.YellowTo;
            }
        }

        /// <summary>
        ///   Stuff processed before adding elements to chart.
        /// </summary>
        protected override void PreDraw()
        {
            // MAGIC NUMBER
            float fontsize = (0.0563636f * this.Border.Height) - 0.8f;
            var defaultFont = TextStyle.Default;
            var font = defaultFont.Font;
            Font labelFont = new Font(font.Name, fontsize);
            defaultFont.Font = labelFont;

            this.Axis.TextStyle = this.SetupFont(this.TextStyle, defaultFont);
            this.FixRanges();
        }
        #endregion

        protected override void CheckFonts()
        {

        }

        public Easing Easing { get; set; }


        protected override string ChartJavascript(Assembly assem, bool minified = true)
        {
            minified = false;

            string values = string.Join(",",
                this.Values.Select(
                    x => string.Format(CultureInfo.InvariantCulture, this.Axis.Format, x)));

            string min = string.Empty;
            if (minified)
            {
                min = ".min";
            }

            string inside = string.Empty;
            string resource = string.Format("Graphix.Scripts.Easings{0}.js", min);
            using (Stream stream = assem.GetManifestResourceStream(resource))
            {
                if (stream != null)
                {
                    using (var reader = new StreamReader(stream))
                    {
                        inside = reader.ReadToEnd();
                    }
                }
            }

            resource = string.Format("Graphix.Scripts.{0}{1}.js", this.GetType().Name, min);
            using (Stream stream = assem.GetManifestResourceStream(resource))
            {
                if (stream != null)
                {
                    using (var reader = new StreamReader(stream))
                    {
                        var temp = reader.ReadToEnd()
                                         .Replace("/*min*/", this.Minimum.ToString(CultureInfo.InvariantCulture))
                                         .Replace("/*max*/", this.Maximum.ToString(CultureInfo.InvariantCulture))
                                         .Replace("/*values*/", values)
                                         .Replace("/*easingMethod*/", FirstCharToLower(this.Easing.ToString()));
                        inside = string.Concat(inside, temp);
                    }
                }
            }
            return inside;
        }

        public static string FirstCharToLower(string input)
        {
            if (string.IsNullOrEmpty(input))
            {
                throw new ArgumentException("ARGH!");
            }

            return input.First().ToString(CultureInfo.InvariantCulture).ToLower()
                   + string.Join(string.Empty, input.Skip(1));
        }


    }

    public enum Easing
    {
        Linear,
        // accelerating from zero velocity
        EaseInQuad,
        // decelerating to zero velocity
        EaseOutQuad,
        // acceleration until halfway, then deceleration
        EaseInOutQuad,
        // accelerating from zero velocity
        EaseInCubic,
        // decelerating to zero velocity
        EaseOutCubic,
        // acceleration until halfway, then deceleration
        EaseInOutCubic,
        // accelerating from zero velocity
        EaseInQuart,
        // decelerating to zero velocity
        EaseOutQuart,
        // acceleration until halfway, then deceleration
        EaseInOutQuart,
        // accelerating from zero velocity
        EaseInQuint,
        // decelerating to zero velocity
        EaseOutQuint,
        // acceleration until halfway, then deceleration
        EaseInOutQuint
    }
}